/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package piccat.view;

import piccat.lib.ImageLoader;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.net.URL;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.Executors;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.OverlayLayout;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import piccat.lib.Grid;
import piccat.lib.Grid.GridIterator;
import piccat.lib.Index;
import piccat.diamondtouch.DTBoundingBoxEvent;
import piccat.diamondtouch.DTListener;
import piccat.diamondtouch.DTSegmentsEvent;
import piccat.lib.CentreLayout;
import piccat.model.Picture;
import piccat.model.ReadyButtonModelWrapper;
import piccat.model.Side;
import piccat.model.Side.PictureAddedEvent;
import piccat.model.Side.PictureDroppedEvent;
import piccat.model.Side.PicturePickupEvent;
import piccat.model.Side.ReadinessChangedEvent;
import piccat.model.Side.SideChangeListener;

/**
 * Class that handles the display of a single "side" of game. Each side
 * contains a grid of cells, in which pictures can be dropped, a button to
 * indicate when player things a new picture should be given, and a cell where
 * the pictures are given.
 * 
 * @author Hamish Morgan
 */
public class SideView extends JPanel {

    /**
     * Log of general system events.
     */
    protected final Logger log = LoggerFactory.getLogger(
            this.getClass().getName());
    /**
     * Log of special user events for study analysis.
     */
    protected final Logger userLog = LoggerFactory.getLogger("piccat.userLog");
    private final ResourceBundle strings = ResourceBundle.getBundle(
            "strings", Locale.getDefault());
    /**
     * The time in milliseconds that the button will flash red if it has been
     * pressed at an incorrect time.
     */
    private static final int BUTTON_FAIL_TIME = 1000;
    //
    private final List<Integer> userIds;
    private final ComponentBoundsBinder boundsBinder;
    private final SideChangeListener listener;
    private final MouseInput mouse;
    private final TouchHandler touchHandler;
    private final GlassPane glass;
    //
    private JPanel grid;
    private Cell newImage;
    private Side model;
    private Grid<Cell> cells;
    private JToggleButton button;
    private ImageIcon buttonIm;
    private ImageIcon buttonImPressed;
    private ImageIcon buttonImSelected;
//    private ImageIcon buttonImFail;
    private ImageIcon buttonImFailPressed;

    public SideView(GlassPane glass, List<Integer> userIds) throws
            IllegalArgumentException {
        this(glass, userIds, null);
    }

    public SideView(GlassPane glassPane, List<Integer> userIds, Side model)
            throws IllegalArgumentException {
        if (glassPane == null)
            throw new IllegalArgumentException(
                    "The glassPane argument must be non-nul.");
        if (userIds == null || userIds.size() == 0)
            throw new IllegalArgumentException(
                    "The userIds argument must be non-null and non-empty.");

        boundsBinder = new ComponentBoundsBinder();
        boundsBinder.setBindLocation(false);
        this.userIds = userIds;
        this.glass = glassPane;
        touchHandler = new TouchHandler();
        listener = new SideChangeHandler();
        mouse = new MouseInput();

        initComponents();
        setModel(model);
    }

    // stuff that should happen exactly once in the life of the panel
    private void initComponents() {
        setBorder(new EmptyBorder(20, 20, 20, 20));
        setOpaque(false);
        setLayout(new OverlayLayout(this));

        newImage = new Cell();
        newImage.setBorder(new EmptyBorder(0, 10, 0, 10));
        newImage.setRendering(false);

        button = new JToggleButton();
        button.setText(strings.getString("AGREE_BUTTON_TEXT"));
        button.setFocusPainted(false);
        button.setText(null);
        button.setBorderPainted(false);
        button.setFocusPainted(false);
        button.setHorizontalAlignment(JToggleButton.CENTER);
        button.setVerticalAlignment(JToggleButton.CENTER);
        button.setMargin(new Insets(0, 0, 0, 0));
        button.setIconTextGap(0);
        button.setBorder(new EmptyBorder(0, 0, 0, 0));

        final URL url = this.getClass().getResource(
                "resources/WeAgreeButton.png");
        final URL urls = this.getClass().getResource(
                "resources/WeAgreeButton-selected.png");
//        final URL urlf = this.getClass().getResource(
//                "resources/WeAgreeButton-fail.png");
        final URL urlp = this.getClass().getResource(
                "resources/WeAgreeButton-pressed.png");
        final URL urlfp = this.getClass().getResource(
                "resources/WeAgreeButton-fail-pressed.png");
        final Dimension size = new Dimension(100, 100);
        buttonIm = new ImageIcon(ImageLoader.getImage(url, size));
        buttonImSelected = new ImageIcon(ImageLoader.getImage(urls, size));
        buttonImPressed = new ImageIcon(ImageLoader.getImage(urlp, size));
//        buttonImFail = new ImageIcon(ImageLoader.getImage(urlf, size));
        buttonImFailPressed = new ImageIcon(ImageLoader.getImage(urlfp,
                size));
        button.setIcon(buttonIm);
        button.setPressedIcon(buttonImPressed);
        button.setSelectedIcon(buttonImSelected);

        grid = new GridPanel();
        grid.setBorder(new EmptyBorder(10, 10, 10, 10));
        grid.setOpaque(false);

        final JPanel bg = new JPanel();
        bg.setOpaque(false);
        bg.setLayout(new BorderLayout());
        bg.add(grid, BorderLayout.CENTER);


        JPanel newImagePanel = new JPanel();
        newImagePanel.setOpaque(false);
        newImagePanel.setLayout(new CentreLayout());
        newImagePanel.setBorder(new LineBorder(Color.red, 4));
        newImagePanel.add(newImage);
        boundsBinder.setTo(newImage);

        JPanel buttonPanel = new JPanel();
        buttonPanel.setOpaque(false);
        buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
        buttonPanel.setOpaque(false);
        buttonPanel.add(button, BorderLayout.CENTER);

        JPanel foot = new JPanel();
        foot.setOpaque(false);
        foot.setLayout(new GridLayout(1, 2));
        foot.setBorder(new EmptyBorder(20, 0, 0, 0));
        foot.add(newImagePanel);
        foot.add(buttonPanel);
        bg.add(foot, BorderLayout.SOUTH);

        add(bg);
        clearModel();
    }

    public void doButtonFail() {
        // TODO: Should use a thread pool
        new Thread(new ButtonFlashFailTask()).start();
    }

    private class ButtonFlashFailTask implements Runnable {

        public void run() {
            try {
                button.setPressedIcon(buttonImFailPressed);
                button.setSelectedIcon(buttonImFailPressed);
                final long startTime = System.currentTimeMillis();
                while (button.isSelected() && startTime + BUTTON_FAIL_TIME >
                        System.currentTimeMillis()) {
                    Thread.sleep(10);
                }
            } catch (InterruptedException ex) {
                log.error(null, ex);
            } finally {
                model.setReady(false);
                button.setPressedIcon(buttonImPressed);
                button.setSelectedIcon(buttonImSelected);
            }
        }
    };

    public Side getModel() {
        return model;
    }

    protected void doEndGame() {
        // stop the button
        if(button != null && button.isEnabled()) 
            button.setEnabled(false);
        // disable the model listern
        model.removeChangeListener(listener);
        // disable input
        
        
        

    }

    public void clearModel() {
        cells = null;
        grid.setLayout(null);
        button.setEnabled(false);
        grid.removeAll();
        boundsBinder.setFrom(null);
        if (model != null) {
            model.addChangeListener(listener);
        }
    }

    public void setModel(Side model) {
        if (this.model == model)
            return;

        clearModel();

        this.model = model;

        // start the new side model
        if (model != null) {
            model.addChangeListener(listener);

            cells = new Grid<Cell>(
                    model.getRows(), model.getCols());

            grid.setLayout(new GridLayout(
                    model.getRows(), model.getCols(), 20, 20));

            GridIterator<Cell> it = cells.iterator();
            while (it.hasNext()) {
                it.next();
                final Cell c = new Cell();
                c.setBorder(new EmptyBorder(4, 4, 4, 4));
                it.set(c);
                grid.add(c);
            }

            boundsBinder.setFrom(cells.iterator().next());

            button.setModel(new ReadyButtonModelWrapper(model));

            for (Picture p : model.getPics()) {
                addPicture(p);
            }
        }
    }

    private void addPicture(Picture p) {
        PictureView pv = new PictureView(p);

        pv.setSize(Util.getGridDimensions(grid));
        pv.addMouseListener(mouse);
        pv.addMouseMotionListener(mouse);
        if (model.solutionContains(p)) {
            Index i = model.solutionIndexOf(p);
            cells.get(i).setContents(pv);
        } else {
            newImage.setContents(pv);
            pickupPicture(p, newImage);
        }
    }

    private void pickupPicture(Picture pic, Cell cell) {
        PictureView pv = cell.getContents();
        cell.clearContents();
        glass.add(pv);
        pv.setLocation(SwingUtilities.convertPoint(cell,
                pv.getLocation(), glass));
        glass.setComponentZOrder(pv, 0);
    }

    private void dropPicture(Picture pic, Cell cell) {
        // need to find the pictureview for this picture
        PictureView pv = null;
        for (Component c : glass.getComponents()) {
            if (!c.getClass().equals(PictureView.class)) {
                throw new RuntimeException(
                        "Contents of glass pane should always be PictureView.");
            }
            PictureView cpv = (PictureView) c;
            if (cpv.getModel() == pic) {
                pv = cpv;
                break;
            }
        }
        if (pv == null)
            throw new RuntimeException(
                    "Failed to find the pictureview for the provided picture in the glass pane.");

        glass.remove(pv);
        glass.repaint(pv.getBounds());
        cell.setContents(pv);
    }

    private Index getDropTarget(PictureView pic) {
        Container parent = pic.getParent();
        if (parent != glass)
            throw new RuntimeException(
                    "Expecting the glass pain for parent. Found " + parent);

        double maxArea = 0;
        Index bestCell = null;
        final GridIterator<Cell> it = cells.iterator();
        while (it.hasNext()) {
            final Cell cell = it.next();
            if (cell.getContents() != null)
                continue;

            Rectangle cellBounds = SwingUtilities.convertRectangle(grid, cell.
                    getBounds(), glass);
            Rectangle picBound = pic.getBounds();
            Rectangle intr = cellBounds.intersection(picBound);

            int area = (int) (intr.getWidth() * intr.getHeight());
            if (intr.isEmpty()) area = 0;

            if (area > maxArea) {
                maxArea = area;
                bestCell = it.previousIndex();
            }
        }
        // TODO: potentially don't snap if the intersection area is too small

        return bestCell;
    }

    private void movePictureToStart(PictureView picView) {
        final Point p = SwingUtilities.convertPoint(picView.getParent(),
                picView.getLocation(), this);

        if (p.getX() < 0 || p.getX() + picView.getWidth() > this.getWidth() ||
                p.getY() < 0 || p.getY() + picView.getHeight() > this.
                getHeight()) {
            new Thread(new MovePictureWorker(picView)).start();
        }

    }

    protected DTListener getTouchHandler() {
        return touchHandler;
    }

    /**
     * Find the component that this click is over.
     * @param msg
     * @return
     */
    private Component getComponent(DTBoundingBoxEvent msg) {
        Point p = new Point(msg.getCenterX(), msg.getCenterY());
        Point q = new Point(msg.getCenterX(), msg.getCenterY());
        Point r = new Point(msg.getCenterX(), msg.getCenterY());
        SwingUtilities.convertPointFromScreen(p, glass);
        SwingUtilities.convertPointFromScreen(q, grid);
        SwingUtilities.convertPointFromScreen(r, button);
        Component c = SwingUtilities.getDeepestComponentAt(glass,
                (int) p.getX(), (int) p.getY());
        if (c != glass)
            return c;
        c = SwingUtilities.getDeepestComponentAt(button, (int) r.getX(),
                (int) r.getY());
        if (c == button) {
            return button;
        }
        c = SwingUtilities.getDeepestComponentAt(
                grid, (int) q.getX(), (int) q.getY());
        return c;
    }

    private class MovePictureWorker implements Runnable {

        private final double innertia = 0.95;
        private final long frameDelay = 10;
        private final double minDistance = 1;
        //
        private final PictureView picView;

        public MovePictureWorker(PictureView picView) {
            this.picView = picView;
        }

        public void run() {

            final double x0 = picView.getX();
            final double y0 = picView.getY();

            final Point2D newImageLoc = SwingUtilities.convertPoint(newImage.
                    getParent(), newImage.getLocation(), glass);

            final double x1 = newImageLoc.getX() + (newImage.getWidth() / 2) -
                    (picView.getWidth() / 2);
            final double y1 = newImageLoc.getY() + (newImage.getHeight() / 2) -
                    (picView.getHeight() / 2);

            double x = x0;
            double y = y0;

            while (Math.hypot(x - x1, y - y1) > minDistance) {

                x = x * innertia + x1 * (1 - innertia);
                y = y * innertia + y1 * (1 - innertia);
                picView.setLocation((int) x, (int) y);
                try {
                    Thread.sleep(frameDelay);
                } catch (InterruptedException ex) {
                    log.error(null, ex);
                }

                if (((int) x) != picView.getX() || ((int) y) != picView.getY()) {
                    // Something else has moved the picture. This could be
                    // user input or another move task. Either way we should
                    // give up on this one to avoid conflicts.
                    break;
                }
            }

            picView.setLocation((int) x1, (int) y1);
        }
    }

    private class MouseInput implements MouseListener, MouseMotionListener {

        private MouseEvent previousEvent;
        private PictureView dragging;
        private Index target;

        public MouseInput() {
            dragging = null;
            target = null;
        }

        private void checkEvent(MouseEvent e) {
            if (!(e.getSource() instanceof PictureView))
                throw new RuntimeException("Expecting a PictureView object.");
        }

        private void updateDropTarget() {
            final Index newTarget = getDropTarget(dragging);
            if (newTarget == target)
                return;
            if (target != null)
                cells.get(target).setTarget(false);
            target = newTarget;
            if (target != null)
                cells.get(target).setTarget(true);
        }

        public void mouseClicked(MouseEvent e) {
        }

        public void mousePressed(MouseEvent e) {
            checkEvent(e);
            if (userLog.isDebugEnabled()) {
                Point p = e.getPoint();
                SwingUtilities.convertPointToScreen(p, e.getComponent());
                userLog.debug("Mouse button pressed at ({},{}).",
                        new Object[]{(int) p.getX(), (int) p.getY()});
            }
            dragging = (PictureView) e.getSource();
            previousEvent = e;

            model.pickup(dragging.getModel());
        }

        public void mouseReleased(MouseEvent e) {
            checkEvent(e);
            if (userLog.isDebugEnabled()) {
                Point p = e.getPoint();
                SwingUtilities.convertPointToScreen(p, e.getComponent());
                userLog.debug("Mouse button released at ({},{}).",
                        new Object[]{(int) p.getX(), (int) p.getY()});
            }
            previousEvent = null;

            if (target == null) {
                // there is no active drop target

                movePictureToStart(dragging);

            } else {
                // there is an active drop target
                cells.get(target).setTarget(false);
                model.drop(target.getRow(), target.getCol(),
                        dragging.getModel());
                dragging = null;
            }
        }

        public void mouseEntered(MouseEvent e) {
        }

        public void mouseExited(MouseEvent e) {
        }

        public void mouseDragged(MouseEvent e) {
            checkEvent(e);
            if (userLog.isDebugEnabled()) {
                Point p = e.getPoint();
                SwingUtilities.convertPointToScreen(p, e.getComponent());
                userLog.debug("Mouse dragged to ({},{}).",
                        new Object[]{(int) p.getX(), (int) p.getY()});
            }
            int dx = e.getX() - previousEvent.getX();
            int dy = e.getY() - previousEvent.getY();

            dragging.setLocation(
                    dragging.getX() + dx,
                    dragging.getY() + dy);


            updateDropTarget();
        }

        public void mouseMoved(MouseEvent e) {
            if (userLog.isDebugEnabled()) {
                Point p = e.getPoint();
                SwingUtilities.convertPointToScreen(p, e.getComponent());
                userLog.debug("Mouse moved to ({},{}).",
                        new Object[]{(int) p.getX(), (int) p.getY()});
            }
        }
    }

    protected class TouchHandler implements DTListener {

        private final Map<Integer, DTBoundingBoxEvent> previousEvent =
                new HashMap<Integer, DTBoundingBoxEvent>();
        private final Map<Integer, PictureView> dragging =
                new HashMap<Integer, PictureView>();
        private final Map<Integer, Index> targets =
                new HashMap<Integer, Index>();

        public void segmentsReceived(DTSegmentsEvent msg) {
            int id = msg.getUserId();
            if (!userIds.contains(new Integer(id)))
                return;
            // Ignore this message type
        }

        private void updateDropTarget() {
            for (int id : dragging.keySet()) {
                PictureView drag = dragging.get(id);
                Index targ = targets.get(id);
                final Index newTarget = getDropTarget(drag);
                if (newTarget == targ)
                    return;
                if (targ != null)
                    cells.get(targ).setTarget(false);
                targ = newTarget;
                if (targ != null)
                    cells.get(targ).setTarget(true);
                targets.put(id, targ);
            }
        }

        public void boundingBoxDown(DTBoundingBoxEvent msg) {
            int id = msg.getUserId();
            if (!userIds.contains(new Integer(id)))
                return;
            previousEvent.put(msg.getUserId(), msg);
            Component c = getComponent(msg);
            if (c == button)
                button.doClick();


            if (userLog.isDebugEnabled()) {
                Point p = msg.getCenterPoint();
                if (p != null && c != null) {
                    SwingUtilities.convertPointToScreen(p, c);
                    userLog.debug("Player #{} touched down at ({},{}).",
                            new Object[]{model.getPlayerId(), (int) p.getX(),
                                (int) p.getY()});
                }
            }

            // XXX: Fix because events sometimes showed the parent or child
            // of a picture view.
            if (c instanceof JLabel)
                c = c.getParent();
            else if (c instanceof Cell)
                c = ((Cell) c).getContents();

            if (c instanceof PictureView) {
                PictureView drag = (PictureView) c;
                if (!model.containsPic(drag.getModel()))
                    return;

                dragging.put(msg.getUserId(), drag);
                model.pickup(drag.getModel());
            }

        }

        public void boundingBoxMoved(DTBoundingBoxEvent msg) {
            int id = msg.getUserId();
            if (!userIds.contains(new Integer(id)))
                return;
            DTBoundingBoxEvent prev = previousEvent.get(msg.getUserId());
            PictureView drag = dragging.get(msg.getUserId());
            if (drag == null)
                return;
            if (userLog.isDebugEnabled()) {
                Component c = getComponent(msg);
                Point p = msg.getCenterPoint();
                if (p != null && c != null) {
                    SwingUtilities.convertPointToScreen(p, c);
                    userLog.debug("Player #{} touched moved to ({},{}).",
                            new Object[]{model.getPlayerId(), (int) p.getX(),
                                (int) p.getY()});
                }
            }

            int dx = msg.getCenterX() - prev.getCenterX();
            int dy = msg.getCenterY() - prev.getCenterY();

            drag.setLocation(
                    drag.getX() + dx,
                    drag.getY() + dy);

            updateDropTarget();
            glass.getParent().repaint();

            previousEvent.put(msg.getUserId(), msg);
        }

        public void boundingBoxUp(DTBoundingBoxEvent msg) {
            int id = msg.getUserId();
            if (!userIds.contains(new Integer(id)))
                return;
            if (userLog.isDebugEnabled()) {
                Component c = getComponent(msg);
                Point p = msg.getCenterPoint();
                if (p != null && c != null) {
                    SwingUtilities.convertPointToScreen(p, c);
                    userLog.debug("Player #{} touched up at ({},{}).",
                            new Object[]{model.getPlayerId(), (int) p.getX(),
                                (int) p.getY()});
                }
            }

            if (targets.containsKey(id) && targets.get(id) != null) {
                cells.get(targets.get(id)).setTarget(false);
                model.drop(targets.get(id).getRow(), targets.get(id).getCol(),
                        dragging.get(id).getModel());
            } else if (dragging.containsKey(id) && dragging.get(id) !=
                    null) {

                movePictureToStart(dragging.get(id));

            }
            dragging.remove(id);
            previousEvent.remove(id);
            dragging.remove(id);
            targets.remove(id);

        }
    }

    private class SideChangeHandler implements SideChangeListener {

        public void pictureAdded(PictureAddedEvent e) {
            checkSource(e);
            addPicture(e.getPicture());
        }

        public void readinessChanged(ReadinessChangedEvent e) {
            // not a sausage
        }

        public void picturePickedUp(PicturePickupEvent e) {
            checkSource(e);
            Picture p = e.getPicture();
            Index i = e.getIndex();
            pickupPicture(p, cells.get(i));
        }

        public void pictureDropped(PictureDroppedEvent e) {
            checkSource(e);
            Picture p = e.getPicture();
            Index i = e.getIndex();
            dropPicture(p, cells.get(i));
        }

        private void checkSource(EventObject e) {
            if (e.getSource() != model)
                throw new IllegalArgumentException(
                        "Recieved event from the wrong side model.");
        }
    }
}
